package processor;

import java.math.RoundingMode;
import java.util.Date;
import java.util.List;

import javax.persistence.LockModeType;

import models.Loan;
import models.LoanApp;
import models.LoanInFunding;
import models.Order;
import models.OrderDetail;
import models.Pay;
import models.Transaction;
import models.Pay.Status;
import models.Transaction.Type;
import play.db.jpa.JPA;
import play.libs.F.Callback0;
import prefix.common.SystemActor;
import prefix.loan.util.LoanMaturity;
import prefix.platform.Constants;
import prefix.platform.loan.AmortizationTable;
import prefix.platform.loan.LoanClass;
import prefix.transaction.BasicTransaction;
import prefix.transaction.TransactionFactory;
import prefix.util.DateUtil;
import prefix.util.JPAUtil;
import prefix.util.OverallUtil;
import repository.LoanRepository;
import repository.OrderRepository;
import repository.PayRepository;
import repository.TransactionRepository;
import service.ActorService;
import service.LoanAppService;
import service.LoanService;
import service.PayService;
import service.exception.LoanException;
import service.exception.LoanException.CODE;
import service.exception.PayException;
import service.impl.LoanServiceImpl;
import service.impl.PayServiceImpl;

public class LoanProcessor extends LoanServiceImpl {

    public LoanProcessor(Loan loan) {
        super(loan);
    }

    public void submitLoan(LoanAppService loanApp, ActorService actor) throws LoanException {
        if (actor == null || actor.getDBActor() == null)
            throw new LoanException(CODE.INVALIDE_ACTOR);
        if (loanApp == null || loanApp.getDBLoanApp() == null)
            throw new LoanException(CODE.INVALIDE_LOANAPP);
        try {
            JPAUtil.beginTransaction();
            this.dbloan.setAid(actor.getDBActor().getId());
            this.dbloan.setLoanAppId(loanApp.getDBLoanApp().getId());
            this.dbloan.setAppAmount(loanApp.getDBLoanApp().getAmount());
            this.dbloan.setName(loanApp.getDBLoanApp().getName());
            this.dbloan.setAppDuration(loanApp.getDBLoanApp().getDuration());
            this.dbloan.setIntRate(0.0);
            this.dbloan.setLoanType(loanApp.getDBLoanApp().getLoanType());
            this.dbloan.setStatus(Loan.Status.NEW);
            this.dbloan.setAppDuration(loanApp.getDBLoanApp().getDuration());
            JPAUtil.em().persist(this.dbloan);
            loanApp.getDBLoanApp().setLoanId(this.dbloan.getId());
            Date now = new Date();
            loanApp.getDBLoanApp().setAppD(now);
            loanApp.getDBLoanApp().setAppExpireD(new Date(now.getTime() + Constants.LOAN_APP_DURATION));
            loanApp.getDBLoanApp().setStatus(LoanApp.Status.SUBMITTED);
            JPAUtil.em().merge(loanApp.getDBLoanApp());

        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            throw new LoanException(CODE.UNKNOWN, e);
        } finally {
            JPAUtil.closeEM();
        }
    }

    public void putLoanInFunding() throws LoanException {
        try {
            JPAUtil.beginTransaction();
            LoanApp app = JPAUtil.em().find(LoanApp.class, this.dbloan.getLoanAppId());
            app.setAppExpireD(new Date(new Date().getTime() + Constants.LOAN_APP_DURATION));
            app.setUpdateD(new Date());
            this.dbloan.setStatus(Loan.Status.INFUNDING);
            this.dbloan.setUpdateD(new Date());
            this.dbloan.setStatusD(new Date());
            LoanInFunding inFunding = new LoanInFunding(this.dbloan.getAid(), this.dbloan.getId(),
                    this.dbloan.getAppAmount(), app.getAppD());
            JPAUtil.em().persist(inFunding);
            JPAUtil.em().merge(this.dbloan);
            JPAUtil.commitTransaction();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
            throw new LoanException(CODE.UNKNOWN, e);
        } finally {
            JPAUtil.closeEM();
        }
    }

    public void assignLoanClass(LoanClass loanClass, LoanMaturity maturity) throws LoanException {
        try {
            JPAUtil.beginTransaction();
            this.dbloan.setGrade(loanClass.getGrade().toString());
            this.dbloan.setIntRate(loanClass.getRATE());
            this.dbloan.setSubGrade(loanClass.getClassName());
            this.dbloan.setDuration(maturity.NUMBER_OF_MONTHS);
            this.dbloan.setStatus(Loan.Status.READYTOFUND);
            this.dbloan.setStartD(new Date());
            this.dbloan.setServiceFee(loanClass.getServiceFeeRate());
            this.dbloan.setManagementFee(loanClass.getManagementFeeRate());
            this.dbloan.setOrigFee(loanClass.getOriginationFeeRate());
            this.dbloan.setUpdateD(new Date());
            JPAUtil.em().merge(this.dbloan);
            JPAUtil.commitTransaction();
        } catch (Exception e) {
            // TODO Auto-generated catch block
            e.printStackTrace();

            throw new LoanException(CODE.ERROR_IN_ASSIGNLOANCLASS, e);
        } finally {
            JPAUtil.closeEM();
        }
    }

    public void issueLoan() {

        try {
            JPAUtil.beginTransaction();
            if (!this.validateFunding())
                throw new LoanException(CODE.INVALID_FUNDING);
            this.dbloan = JPAUtil.em().find(Loan.class, this.dbloan.getId(), LockModeType.PESSIMISTIC_WRITE);
            Date now = new Date();
            if (DateUtil.isExcludedDate(now))
                throw new LoanException(CODE.INVALID_ISSUE_DATE);
            if (now.after(DateUtil.getLoanIssuingCutOffTime(now)))
                throw new LoanException(CODE.AFTER_CUTOFF_TIME);
            if (!Loan.Status.REVIEW_FINAL.equals(this.dbloan.getStatus()))
                throw new LoanException(CODE.INVALID_STATUS);
            //            LoanInFunding lif = LoanRepository.getSingleByProperty(LoanInFunding.class, "loanId", this.dbloan.getId(),
            //                    null);
            //            JPAUtil.em().remove(lif);

            List<OrderDetail> detailList = OrderRepository.getListByProperties(OrderDetail.class, new String[] {
                    "loanId", "status" }, new Object[] { this.dbloan.getId(), OrderDetail.Status.PENDING.ordinal() });
            LoanApp app = this.dbloan.getLoanApp();
            for (OrderDetail detail : detailList) {
                double rpa = AmortizationTable.getMonthlyPayment(detail.getAmount(), this.dbloan.getDuration(),
                        this.dbloan.getIntRate(), app.getPaymentMode().ordinal());
                detail.setRPA(OverallUtil.round2Decimal(rpa, RoundingMode.FLOOR).doubleValue());
                double accrueAmount = rpa*this.dbloan.getDuration();
                detail.setAccrueAmount(accrueAmount);
                Order order = detail.getOrder();
                BasicTransaction t2 = TransactionFactory.createTransaction(
                        Transaction.Type.T2_LENDER_ISSUE_FUNDS_TO_BORROWER, order.getAid(), order.getAid(),
                        this.dbloan.getAid(), null, null, detail.getAmount(), now, order.getId(), this.dbloan.getId());
                JPAUtil.em().persist(t2.getDbTrans());
            }

            // orignation fee
            double orignFeeRate = this.dbloan.getOrigFee();
            double orignFee = LoanServiceImpl.getAcceptedOriginationFee(this.getCommitAmount(), orignFeeRate);
            double toBorrower = this.getCommitAmount() - orignFee;

            BasicTransaction t4 = TransactionFactory.createTransaction(Transaction.Type.T4_BORROWER_PAY_ORIG_FEE,
                    this.dbloan.getAid(), this.dbloan.getAid(), SystemActor.ORIGINATION_FEE_ACCOUNT_W2.getId(), null,
                    null, orignFee, now, null, this.dbloan.getId());
            JPAUtil.em().persist(t4.getDbTrans());

            // push funds to borrower bank account

            BasicTransaction t3 = TransactionFactory.createTransaction(Type.T3_PUSH_FUNDS_TO_BORROWER_BANK,
                    this.dbloan.getAid(), this.dbloan.getAid(), SystemActor.PUSH_FUNDS_WALLET_W8.getId(), null, null,
                    toBorrower, now, null, this.dbloan.getId());
            JPAUtil.em().persist(t3.getDbTrans());

            double rpa = AmortizationTable.getMonthlyPayment(this.dbloan.getAmount(), this.dbloan.getDuration(),
                    this.dbloan.getIntRate(), app.getPaymentMode().ordinal());
            rpa = OverallUtil.round2Decimal(rpa, RoundingMode.CEILING).doubleValue();
            this.dbloan.setRpa(rpa);
            this.dbloan.setStartD(now);
            this.dbloan.setStatus(Loan.Status.ISSUING);
            this.dbloan.setUpdateD(now);

            // create payment
            Date dueDate = DateUtil.addMonths(now, 1);
            dueDate = DateUtil.normalizeDay(dueDate);
            Pay pay = this.getNextPay(null);
            pay.setDueD(dueDate);
            pay.setStatus(Pay.Status.FUTURE);
            pay.setLoanId(this.dbloan.getId());
            PayService payService = new PayServiceImpl(pay);
            pay.setDueAmount(payService.getAmountToBeCurrent());
            JPAUtil.em().persist(pay);
            this.dbloan.setCurrentPay(pay.getId());
            JPAUtil.commitTransaction();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            JPAUtil.closeEM();
        }

    }

    public void executeIssueTransaction() {
        List<Transaction> list = TransactionRepository.getLoanIssueTransactions(this.dbloan.getId());

        System.out.println("Transaction to execute : " + list.size());
        for (Transaction trans : list) {
            BasicTransaction.executeNow(trans.getId());
        }
    }

    public void issueAndExecuteLoan() {
        this.issueLoan();
        executeIssueTransaction();
    }

    public static void issueLoans() {
        Date now = new Date();
        if (DateUtil.isExcludedDate(now) || now.after(DateUtil.getLoanIssuingCutOffTime(now)))
            return;
        List<Loan> loanList = LoanRepository.getLoanReadyToIssue();
        System.out.println("Loans to issue = " + loanList.size());

        for (Loan loan : loanList) {
            LoanService loanservice = new LoanServiceImpl(loan);
            loanservice.getProcessor().issueAndExecuteLoan();
        }
    }

    public void validateLoanAndPayments() throws Exception {
        this.validateLoanStatus();
        this.validateLPayTable();
        this.validateLoanStatusAfterPayment();
    }

    public void validateLoanStatusAfterPayment() throws PayException, LoanException {
        Loan.Status afterStatus = this.getLoanStatusAfterPayment();
        Loan.Status currentStatus = this.dbloan.getStatus();
        if (currentStatus == Loan.Status.ISSUING)
            currentStatus = Loan.Status.ISSUED;
        if (afterStatus != currentStatus)
            throw new PayException(PayException.CODE.INVALID_LOAN_STATUS);
    }

    public void validateLoanStatus() throws LoanException {
        if (!this.hasBeenIssued())
            return;

        double unpaidPrincipalBal = LoanRepository.getUnpaidBalance(this.dbloan.getId());
        Loan.Status loanStatus = this.dbloan.getStatus();

        if (unpaidPrincipalBal <= Constants.PRINCIPAL_BALANCE_PENALTY_5CENT
                && !OverallUtil.isIn(loanStatus, Loan.Status.CURRENT, Loan.Status.COMPLETED, Loan.Status.GRACE_CURRENT,
                        Loan.Status.ISSUING, Loan.Status.ISSUED)) {
            throw new LoanException(CODE.INVALID_STATUS);
        }
        if (unpaidPrincipalBal > Constants.PRINCIPAL_BALANCE_PENALTY_5CENT) {
            if (OverallUtil.isIn(loanStatus, Loan.Status.CURRENT))
                throw new LoanException(CODE.INVALID_STATUS);
        }
    }

    public void validateLPayTable() throws PayException {
        Pay pay = this.dbloan.getCurrentPay();

        List<Pay> payList = this.dbloan.getPayList();
        if (!this.hasBeenIssued() && this.dbloan.getStatus() != Loan.Status.ISSUING) {
            validateLPayTableForNonIssued(pay, payList);
        }
        if (this.dbloan.getStatus() == Loan.Status.COMPLETED) {
            validateLPayTableForPaidOff(pay, payList);
            return;
        }

    }

    private void validateLPayTableForPaidOff(Pay current, List<Pay> payList) throws PayException {
        validateSuccessFullLpays(payList);
    }

    public void validateSuccessFullLpays(List<Pay> payList) throws PayException {
        for (Pay lpay : payList) {
            if (lpay.getStatus() == Pay.Status.CLOSE_PAID) {
                validateSuccessFullLpay(lpay);
            }
        }
    }

    private void validateSuccessFullLpay(Pay lpay) throws PayException {
        if (lpay.getDueAmount() > lpay.getReceivedAmount())
            throw new PayException(PayException.CODE.INVALID_RECEIVEAMT);
    }

    private void validateLPayTableForNonIssued(Pay current, List<Pay> payList) throws PayException {

        if (payList.size() != 0)
            throw new PayException(PayException.CODE.INVALID_PAY_LIST);
    }

}
